<?php

namespace Kcdns\Service\Pay\SDK\Driver;

use Kcdns\Service\Pay\SDK\Pay;

abstract class Weixin extends Pay
{

    const NAVITE_PAY_URL = 'weixin://wxpay/bizpayurl?';
    protected $gateway = '';
    protected $apidomain = 'https://api.mch.weixin.qq.com/';

    protected $apiurl_unifiedorder = 'pay/unifiedorder';
    protected $apiurl_orderquery = 'pay/orderquery';


    protected $config = array(
        'appid' => '',
        'mchid' => '',
        'key' => '',
        'notify_url' => ''
    );



    /**
     * 支付通知验证
     */
    public function verifyNotify($notify)
    {

        $isSign = $notify['sign'] == $this->makeSign($notify) ? true : false;

        if (!$isSign) {
            return false;
        }
        $this->checkData($notify);

        if ('return' == I('get.method')) {
            //当同步返回时，获取远程服务器结果，异步时节约一次资源
            $remoteNotify = $this->_orderquery($notify);
        } else {
            $remoteNotify = true;
        }
        if ($remoteNotify) {
            $this->setInfo($notify);
            return $this->info['status'];
        } else {
            return false;
        }
    }

    protected function setInfo($notify)
    {
        $info = array();
        //支付状态
        $info['status'] = ($notify['result_code'] == 'SUCCESS') ? true : false;
        $info['money'] = $notify['total_fee'] / 100;
        $info['out_trade_no'] = $notify['out_trade_no'];
        $info['trade_no'] = $notify['transaction_id'];
        $this->info = $info;
    }

    /**
     * 远程查询订单.
     * @param   type $varname description
     * @return  type    description
     * @access  public or private
     * @static  makes the class property accessible without needing an instantiation of the class
     * @author hayden <hayden@yeah.net>
     */
    protected function _orderquery($data)
    {
        $param['appid'] = $this->config['appid'];
        $param['mch_id'] = $this->config['mchid'];
        $param['out_trade_no'] = $data['out_trade_no'];
        $param['nonce_str'] = md5(rand(100, 999));
        $param['sign'] = $this->makeSign($param);

        $response = self::getResponse($this->apiurl_orderquery, $param);

        return $response['trade_state'] == 'SUCCESS';
    } // end func

    /**
     * 微信统一下单
     */
    abstract function unifiedOrder($paymentInfo = array());

    /**
     * 常规参数统一效验.
     * @param   type $varname description
     * @return  type    description
     * @access  public or private
     * @static  makes the class property accessible without needing an instantiation of the class
     * @author hayden <hayden@yeah.net>
     */
    function checkData($data)
    {
        if ('SUCCESS' !== $data['return_code']) {
            E($data['return_msg']);
        }
        if ('SUCCESS' !== $data['result_code']) {
            E($data['err_code'] . ':' . $data['err_code_des']);
        }
    } // end func

    /**
     * 异步通知验证成功返回信息
     */
    public function notifySuccess()
    {
        $return['return_code'] = 'SUCCESS';
        $return['return_msg'] = 'success';
        echo $this->toXml($return);
    }

    /**
     * 将xml转为array
     */
    public static function parseXml($xml = '')
    {
        if (!$xml) {
            E("远程数据异常！");
        }
        libxml_disable_entity_loader(true);
        return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
    }

    /**
     * 输出xml字符
     */
    public static function toXml($values)
    {
        $xml = "<xml>";
        foreach ($values as $key => $val) {
            if (is_numeric($val)) {
                $xml .= "<" . $key . ">" . $val . "</" . $key . ">";
            } else {
                $xml .= "<" . $key . "><![CDATA[" . $val . "]]></" . $key . ">";
            }
        }
        $xml .= "</xml>";
        return $xml;
    }

    /**
     * 生成签名
     */
    protected function makeSign($param)
    {
        // 签名步骤一：按字典序排序参数
        ksort($param);
        reset($param);
        $string = $this->toUrlParams($param);
        // 签名步骤二：在string后加入KEY
        $string = $string . "&key=" . $this->config['key'];

        // 签名步骤三：MD5加密
        $string = md5($string);
        // 签名步骤四：所有字符转为大写
        $result = strtoupper($string);
        return $result;
    }

    /**
     * 格式化参数格式化成url参数
     */
    protected function toUrlParams($values)
    {
        $buff = "";
        foreach ($values as $k => $v) {
            if ($k != "sign" && $v != "" && !is_array($v)) {
                $buff .= $k . "=" . $v . "&";
            }
        }

        $buff = trim($buff, "&");
        return $buff;
    }

    /**
     * 配置监检测
     */
    public function check()
    {
        if (!$this->config['appid'] || !$this->config['mchid'] || !$this->config['key']) {
            throw new \Exception("微信支付设置有误！");
        }
        return true;
    }


    /**
     * 微信远程提交
     * @param $data 提交的XML信息
     * @return 下单结果
     */
    protected function getResponse($url, $data)
    {
        $xml = self::toXml($data);
        $responseTxt = $this->postXmlCurl($xml, $this->apidomain . $url, false, 6);
        $response = self::parseXml($responseTxt);
        $this->checkData($response);
        return $response;
    }

    /**
     * 以post方式提交xml到对应的接口url
     */
    protected function postXmlCurl($xml, $url, $useCert = false, $second = 30)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_TIMEOUT, $second);
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        curl_setopt($ch, CURLOPT_HEADER, FALSE);
        curl_setopt($ch, CURLOPT_HTTPHEADER, array("Content-Type:text/xml; charset=utf-8"));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
        curl_setopt($ch, CURLOPT_POST, TRUE);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);

        //微信cert设置
        //以下两种方式需选择一种
        if ($useCert) {
            //第一种方法，cert 与 key 分别属于两个.pem文件
            //默认格式为PEM，可以注释
            curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM');
            curl_setopt($ch, CURLOPT_SSLCERT, $this->_getCertPath() . 'apiclient_cert.pem');
            //默认格式为PEM，可以注释
            curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM');
            curl_setopt($ch, CURLOPT_SSLKEY, $this->_getCertPath() . 'apiclient_key.pem');
        }
        $data = curl_exec($ch);
        $errNo = curl_errno($ch);
        if (!$errNo) {
            curl_close($ch);
            return $data;
        } else {
            $errInfo = curl_error($ch);
            curl_close($ch);
            throw new \Exception("支付异常curl出错，错误码:$errNo 错误信息:" . $errInfo);
        }
    }

    /**
     * 取回信
     * @param $type  notify:异步通知数据  return:同步通知数据
     */
    public function getRequest($type = 'notify')
    {
        $iget = I('get.');
        $ipost = I('post.');

        unset($iget['paysn']);
        $response = "";
        switch ($type) {
            //支付同步通知
            case 'return':
                if (IS_POST && !empty($ipost)) {
                    $response = $ipost;
                } elseif (IS_GET && !empty($iget)) {
                    $response = $iget;
                } else {
                    throw new \Exception('Access Denied');
                }
                break;
            //支付异步通知
            case 'notify':
                $notify = file_get_contents('php://input') ?: $GLOBALS['HTTP_RAW_POST_DATA'];
                $response = self::parseXml($notify);
                break;
            //退款同步通知(为本地模拟同步通知）为了解决微信退款在无异步通知情况下 业务流程正常完成
            case 'rreturn':
                $notify = file_get_contents('php://input') ?: $GLOBALS['HTTP_RAW_POST_DATA'];
                $response = self::parseXml($notify);
                break;
            //退款异步通知 需要解密数据
            case 'rnotify':
                $notify = file_get_contents('php://input') ?: $GLOBALS['HTTP_RAW_POST_DATA'];
                $response = self::parseXml($notify);
                break;
            default:
                $response = "";
        }

        return $response;
    }

    /**
     * 生成扫码支付url
     * @return string
     */
    public function createNavitePayUrl($payid)
    {
        $params['appid'] = $this->config['appid'];
        $params['mch_id'] = $this->config['mchid'];
        $params['nonce_str'] = md5(time());
        $params['product_id'] = $payid;
        $params['time_stamp'] = time();
        $params['sign'] = $this->makeSign($params);

        return self::NAVITE_PAY_URL . http_build_query($params);
    }

    /**
     * 取回异步通知后外部订单ID
     */
    public function getOuterNo($notifyInfo = array(), $type = 'pay')
    {
        //支付 返回异步通知的支付方订单号 transaction_id
        //退款 返回异步通知的退款订单号   refund_id
        return $notifyInfo[$type == 'pay' ? 'transaction_id' : 'refund_id'];
    }

    /**
     * 发起退款
     * @param string $outerNo 支付外部单号
     * @param float $refundAmount 退款金额(元)
     * @param string $refundCallback 退款回调
     * @param array $refundRecord 退款记录数据
     * @param array $paymentRecord 退款对应的支付记录
     * @param string(64) $refundNo      退款单号  同一个订单不同次退款 用于指定退款单号 不传默认用订单号自动生成outerNo-YmdHi
     */
    public function refund($outerNo, $refundAmount, $refundRecord = array(), $paymentRecord = array(), $refundNo = '')
    {
        $param = array(
            'appid' => $this->config['appid'],
            'mch_id' => $this->config['mchid'],
            'nonce_str' => md5(time()),
            //'transaction_id'=>$outerNo,//外部订单号
            'out_refund_no' => $refundNo ?: ($refundRecord['order_no'] . '-' . date('YmdHi')),//退款单号
            'out_trade_no' => $refundRecord['order_no'],//商户内部订单号
            'total_fee' => $paymentRecord['amount'] * 100,//原订单支付总金额
            'refund_fee' => $refundAmount * 100,//退款金额
        );
        $param['sign'] = $this->makeSign($param);

        if ($this->config['mode'] == 1) {
            //调试模式获取测试响应
            $responseTxt = $this->getTestResponse($refundAmount, $paymentRecord);
        } else {
            //中转及正式模式 退款请求
            $xml = self::toXml($param);
            $responseTxt = $this->postXmlCurl($xml, 'https://api.mch.weixin.qq.com/secapi/pay/refund', $useCert = true, 6);
        }
        $response = self::parseXml($responseTxt);

        //记录接口请求及响应
        $this->_savePrePayInfo($refundRecord['payment_id'], $param, $responseTxt);
        if ($response['return_code'] == 'SUCCESS') {
            //退款成功
            return $this->refundSuccess($response['transaction_id'], $response['out_trade_no'], $response['refund_fee'] * 100);
        } else {
            throw new \Exception('退款失败:' . $response['err_code_des']);
        }
    }

    //退款同步结果返回
    public function refundSuccess($outer_sn, $order_no, $refundAmount)
    {
        return array(
            'outer_sn' => $outer_sn,
            'order_no' => $order_no,
            'refund_amount' => $refundAmount
        );
    }

    /**
     * 退款(模拟)异步通知数据验证支付通知验证
     */
    public function verifyRNotify($notify)
    {

        return $notify['sign'] == $this->makeSign($notify) ? true : false;
    }

    /**
     * @param $amount
     * @return 解密退款异步通知数据 todo
     */
    public function decryptRefundData()
    {

    }

    //测试支付模拟退款响应
    public function getTestResponse($amount, $paymentInfo)
    {
        $data = array(
            'return_code' => 'SUCCESS',
            'return_msg' => 'OK',
            'appid' => $this->config['appid'],
            'mch_id' => $this->config['mchid'],
            'nonce_str' => time(),
            'transaction_id' => time(),
            'out_trade_no' => $paymentInfo['order_no'],
            'out_refund_no' => time(),
            'refund_id' => time(),
            'refund_fee' => $amount * 100,
        );
        $data['sign'] = $this->makeSign($data);
        return self::toXml($data);
    }

    //获取微信退款所需证书所在路径
    protected function _getCertPath()
    {
        return str_replace('\\', '/', __DIR__) . '/WeixinCert/';
    }
}